﻿// <copyright file="ConnexityBuffer.cs" company="Ecole Normale Supérieure">
// Released under GPLv2
// </copyright>
// <summary>Contains the ConnexityBuffer class</summary>

namespace Sqwarea.GameLogic.Connexity
{
    using System.Collections.Generic;
    using System.Drawing;
    using System.Linq;
    using Exceptions;
    using Extensions;
    using Persistence;

    /// <summary>
    /// Gives the impression of a square by square access, 
    /// while getting loads of square behind the scene.
    /// Also does some caching.
    /// Should be created anew each time a primary method of ConnexityProvider is called.
    /// </summary>
    public class ConnexityBuffer : IConnexityBuffer
    {
        /// <summary>
        /// Used internally for all caching.
        /// </summary>
        private readonly Dictionary<Point, Square> _buffer;

        /// <summary>
        /// The set of all retrieved partitions
        /// </summary>
        private readonly HashSet<string> _partitions;

        /// <summary>
        /// Set of modified squares (used by Commit and UpdateSquare)
        /// And for each, its former king.
        /// </summary>
        private readonly Dictionary<Point, long> _modifiedSquares;

        /// <summary>
        /// The provider this class is a wrapper upon.
        /// </summary>
        private readonly IGameProvider _gameProvider;

        /// <summary>
        /// For each king, gives the number of squares he has lost.
        /// Used in the Commit step
        /// </summary>
        private readonly Dictionary<long, int> _lostSquares;

        /// <summary>
        /// False iff something has failed during commit (for it to be logged by ConnexityProvider).
        /// </summary>
        private bool _isCommitOk;

        /// <summary>
        /// Initializes a new instance of the ConnexityBuffer class
        /// </summary>
        /// <param name="gameProvider">The provider this class is a wrapper upon</param>
        public ConnexityBuffer(IGameProvider gameProvider)
        {
            _buffer = new Dictionary<Point, Square>();
            _modifiedSquares = new Dictionary<Point, long>();
            _partitions = new HashSet<string>();
            _gameProvider = gameProvider;
            _lostSquares = new Dictionary<long, int>();
            _isCommitOk = true;
        }

        /// <summary>
        /// Main method of this class, 
        /// has the same semantics as IGameProvider.GetSquareByPosition,
        /// but uses a buffer for increasing the performance of ConnexityProvider
        /// WARNING : The square it returns is a reference to the buffer.
        /// So all changes to this square are instantly stored to the buffer.
        /// But it is still needed to do UpdateSquare for it to be commited once AtomicCommit is called
        /// </summary>
        /// <param name="position">The coordinates of the Square to be gotten</param>
        /// <returns>The Square required</returns>
        public Square GetSquareByPosition(Point position)
        {
            Square square;
            if (!_partitions.Contains(_gameProvider.GetPartitionKey(position)))
            {
                var squaresList = _gameProvider.GetSquaresInPartition(position);

                foreach (Square square2 in squaresList)
                {
                    if (!_buffer.ContainsKey(square2.Position))
                    {
                        _buffer.Add(square2.Position, square2);
                    }
                }

                _partitions.Add(_gameProvider.GetPartitionKey(position));
            }

            if (!_buffer.TryGetValue(position, out square))
            {
                square = new Square(position);
            }

            if (square == null)
            {
                throw new ConnexityHardException();
            }

            return square;
        }

        /// <summary>
        /// Gives an access to the neighbours of a given position
        /// </summary>
        /// <param name="position">The position whose neighbours are asked for</param>
        /// <returns>A IEnumerable of the neighbours squares</returns>
        public NeighbourSquares GetNeighbours(Point position)
        {
            return new NeighbourSquares(this, position);
        }

        /// <summary>
        /// Gives an access to the neighbours of a given square
        /// </summary>
        /// <param name="square">The square whose neighbours are asked for</param>
        /// <returns>A IEnumerable of the neighbours squares</returns>
        public NeighbourSquares GetNeighbours(Square square)
        {
            return GetNeighbours(square.Position);
        }

        /// <summary>
        /// Update the information on the square defined by its internal position
        /// </summary>
        /// <param name="square">Square to update (containing position)</param>
        public void UpdateSquare(Square square)
        {
            _buffer.Remove(square.Position);
            _buffer.Add(square.Position, square);

            long kingId = _gameProvider.GetSquareByPosition(square.Position).KingId;
            if (!_modifiedSquares.ContainsKey(square.Position))
            {
                _modifiedSquares.Add(square.Position, kingId);
            }
        }

        /// <summary>
        /// Sends all the modification done to the squares to the IGameProvider.
        /// Send only modified squares.
        /// </summary>
        public void Commit()
        {
            _gameProvider.UpdateSquares(from pair in _modifiedSquares select _buffer[pair.Key]);
        }

        /// <summary>
        /// Sends all the modification done to the squares to the IGameProvider.
        /// Send only modified squares.
        /// Also update the corresponding king(s) to make them have the right number of squares.
        /// </summary>
        /// <returns>True iff everything has gone ok</returns>
        public bool AtomicCommit()
        {
            foreach (var pair in _modifiedSquares)
            {
                _gameProvider.UpdateSquare(pair.Key, CommitSquare);
            }
            
            foreach (var pair in _lostSquares)
            {
                _gameProvider.UpdateKing(pair.Key, CommitKing);
            }

            return _isCommitOk;
        }

        /// <summary>
        /// This method is used by Commit2 to update atomically a Square.
        /// </summary>
        /// <param name="oldSquare">The square before its update</param>
        /// <returns>The square after its update</returns>
        private Square CommitSquare(Square oldSquare)
        {
            // Verification that data can be retrieved););
            if (null == oldSquare)
            {
                throw new ConnexityHardException("ConnexityBuffer.CommitSquare : oldSquare is null");
            }

            Square newSquare;
            if (! _buffer.TryGetValue(oldSquare.Position, out newSquare) || null == newSquare)
            {
                throw new ConnexityLogicException("ConnexityBuffer.CommitSquare : square unretrievable in the buffer");
            }

            // In case the square has been attacked during the process
            long kingId = oldSquare.KingId;
            if (kingId != _modifiedSquares[oldSquare.Position])
            {
                _isCommitOk = false;
                return oldSquare;
            }

            // In case the square has been reinforced during the process
            newSquare.Troops = oldSquare.Troops;

            if (newSquare.KingId != kingId)
            {
                // To have the number of squares of the king right
                int numberOfLostSquares;
                if (_lostSquares.TryGetValue(kingId, out numberOfLostSquares))
                {
                    _lostSquares.Remove(kingId);
                    numberOfLostSquares++;
                }
                else
                {
                    numberOfLostSquares = 1;
                }

                _lostSquares.Add(kingId, numberOfLostSquares);
            }

            return newSquare;
        }

        /// <summary>
        /// This method is used by Commit2 to atomically update the number of squares of a king.
        /// </summary>
        /// <param name="king">The king to be updated</param>
        /// <returns>The king once updated</returns>
        private King CommitKing(King king)
        {
            int numberOfLostSquares;
            if (! _lostSquares.TryGetValue(king.Id, out numberOfLostSquares))
            {
                throw new ConnexityLogicException("ConnexityBuffer.CommitKing : number of lost squares not in the dictionnary");
            }

            king.UpdateAcademyTroops();
            king.SquaresNumber -= numberOfLostSquares;
            return king;
        }
    }
}
